package Main.TcpBreakNat;

import Main.pojo.ClientInfo;
import Main.pojo.TcpMessage;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Scanner;

/**
 * PACKAGE_NAME: Main.TcpBreakNat
 * Description:
 * 连接用户流程：
 * 1、A从中央获取有那些用户，并且获取常驻通讯
 * 2、A新建一个Socket与Center建立连接，并告诉中央要连接B，通过B的UUID
 * 3、中央记录A外网IP与端口，并为这个socket新newUUID,通知B：ip,port,uuid,需要与你建立连接。
 * 4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
 * 5、中央记录ip,port。通知A：B联系上了，它的ip\port，请做好准备。通知B：A已通知，请连接它
 * 6、A先试探的用原端口，请求下B5次，100ms一次（为了让NAR给B留个记录）
 * 7、A用原端口创建ServerSocket
 * 8、500ms后，B用指定原端口尝试连接5次，400ms一次，如果还是失败，就用再该端口通过中央告诉原主机。
 * 9、一般这个时候就已经连接成功，先测试，如果成功就不考虑后面
 */
public class Client {
    public static void main(String[] args) throws IOException {
        TCOConnectWithCenter clientWithCenter = new TCOConnectWithCenter();
        // 开启线程处理接受的信息
        new Thread(clientWithCenter).start();
        System.out.println("开始请求所有主机列表！");

        String requestCode = "请求操作编号：\n" +
                "1 获取所有clientInfo\n" +
                "2 与某打洞\n" +
                "请输入：";
        System.out.print(requestCode);
        Scanner scanner = new Scanner(System.in);
        while (true) {
            int code = scanner.nextInt();
            clientWithCenter.request(code);
        }

    }
}

/**
 * 客户端与服务器保持连接的部分
 */
class TCOConnectWithCenter implements Runnable {
    Socket socket;
    ObjectOutputStream objectOutputStream;
    ObjectInputStream objectInputStream;

    public TCOConnectWithCenter() throws IOException {
        this.socket = new Socket("192.168.31.40", 9999);
        System.out.println("与中央建立了连接，本地Port:" + socket.getLocalPort());
        this.objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
        this.objectInputStream = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));
    }


    public void request(int type, Object... objs) throws IOException {
        /*
        request code:
        1 获取所有clientInfo
         */
        TcpMessage tcpMessage = new TcpMessage();
        switch (type) {
            case 1:
                tcpMessage.code = 1;
                break;
            case 2:
                Scanner scanner = new Scanner(System.in);
                System.out.print("请输出目标UUID:");
                String uuid = scanner.next();
                System.out.println("收到：" + uuid);
                // A新建一个Socket与Center建立连接，(在新建的连接里面)并告诉中央要连接B，通过B的UUID
                TCPConnectWithBreakNat AClient = new TCPConnectWithBreakNat(uuid);
                new Thread(AClient).start();
                break;

        }
        this.objectOutputStream.writeObject(tcpMessage);
        this.objectOutputStream.flush();
    }

    public void handleMsgFromCenter(TcpMessage tcpMessage) throws IOException {
        /*
        1001 返回当前所有已连接客户的信息
        1002 有人请求TCP与我连接
         */
        // 线程开始处理信息
        System.out.println("收到信息[" + LocalDateTime.now() + "]：");
        switch (tcpMessage.code) {
            case 1001: // 1001 返回当前所有已连接客户的信息
                ClientInfo[] clientInfos = (ClientInfo[]) tcpMessage.data;
                for (ClientInfo clientInfo : clientInfos) {
                    System.out.println(clientInfo);
                }
                break;
            case 1002:
                /*
                3、中央记录A外网IP与端口，并为这个socket新newUUID,通知B：uuid,ip,port,需要与你建立连接。
                4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
                 */
                List data = (List) tcpMessage.data;
                new Thread(new TCPConnectWithBreakNat((String) data.get(0), (String) data.get(1), (int) data.get(2))).start();
                break;
        }
    }

    @Override
    public void run() { // 线程处理接受到的信息
        while (true) {
            try {
                TcpMessage tcpMessage = (TcpMessage) objectInputStream.readObject();
                // 开启多线程处理信息
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            handleMsgFromCenter(tcpMessage);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}


class TCPConnectWithBreakNat implements Runnable {
    // 预打洞TCP外部信息
    String targetUUID;
    String targetIp;
    int targetPort;

    // 服务端的
    ServerSocket serverSocket;

    // 客户端的
    Socket socket;
    // 客户端的 TCP连接对头的in out流
    ObjectInputStream objectInputStream;
    ObjectOutputStream objectOutputStream;

    // 其他信息
    int localPort; // 非常重要，保证socket或Server的NAT内端口不变，那么NAT外端口不变
    boolean isOk = false;
    boolean isActive = true;

    // B
    public TCPConnectWithBreakNat(String AUUID, String AIp, int APort) throws IOException {
        targetUUID = AUUID;
        targetIp = AIp;
        targetPort = APort;
        // 连接主机,自动更新 输入输出流引用
        toConnect("192.168.31.40", 9999);
        // 4、B新建一个Socket与Center建立连接，告诉中央：这个连接，是为UUID的A连接做准备的。
        TcpMessage tcpMessage = new TcpMessage();
        tcpMessage.code = 3;
        tcpMessage.data = AUUID;
        objectOutputStream.writeObject(tcpMessage);
        objectOutputStream.flush();
    }

    // A
    public TCPConnectWithBreakNat(String BUUID) throws IOException {
        // 目标B UUID
        this.targetUUID = BUUID;
        // 连接主机,自动更新 输入输出流引用
        toConnect("192.168.31.40", 9999);
        // 告诉中央，我要连接B
        TcpMessage tcpMessage = new TcpMessage();
        tcpMessage.code = 2;
        tcpMessage.data = BUUID;
        objectOutputStream.writeObject(tcpMessage);
        objectOutputStream.flush();
    }

    /**
     * 连接服务器用的，本地随机一个端口就可以了，一定需要连接上，所以不需要设置短的超时时间
     *
     * @throws IOException
     */
    public void toConnect(String ip, int port) throws IOException {
        // 2s 默认连接超时时间
        this.toConnect(ip, port, 2000, -1);
    }

    /**
     * A 3次测试连接B的时候用的，需要设置短的超时时间，以及绑定内部端口为原来端口
     * A 创建ServerSocket 的时候也需要绑定这个 local port
     */
    public void toConnect(String ip, int port, int timeOut, int localPort) throws IOException {
        socket = new Socket();
        socket.setReuseAddress(true);
        if (localPort != -1) { // 代表是第一次连接服务器用的，需要随机分配一个内部端口
            socket.bind(new InetSocketAddress(localPort));
        }
        socket.connect(new InetSocketAddress(ip, port), timeOut);
        this.localPort = socket.getLocalPort();
        objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
        objectInputStream = new ObjectInputStream(socket.getInputStream());
    }

    /**
     * 此变成服务器
     */
    public void toCreateServiceSocket(int localPort) throws IOException {
        if (this.socket != null && !this.socket.isClosed()) {
            this.toClose();
        }
        this.localPort = localPort; // 可以去掉这一句
        serverSocket = new ServerSocket();
        serverSocket.setReuseAddress(true);
        // 绑定了就会启动监控
        System.out.println("参数localPort:" + localPort);
        serverSocket.bind(new InetSocketAddress(localPort));
        System.out.println("A开始变成服务器！PORT：" + localPort);
    }

    public void toClose() throws IOException {
        objectOutputStream.close();
        objectInputStream.close();
        socket.close();
    }

    @Override
    public void run() { // 处理收到的消息
        while (true) {
            if (!isActive)
                break;
            try {
                TcpMessage tcpMessage = (TcpMessage) objectInputStream.readObject();
                // 开启处理
                handleMsg(tcpMessage);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public void handleMsg(TcpMessage tcpMessage) throws IOException, InterruptedException {
        /*
        breakTcp 收到的信息
        2001 中央通知A：B联系上了，它的ip\port，请做好准备
        2002 中央通知B：A已通知，请连接它
        2003 B联系A：我成功联系上你了！！！！
         */
        switch (tcpMessage.code) {
            case 2001:
                Object[] dataArray = (Object[]) tcpMessage.data;
                targetIp = (String) dataArray[0];
                targetPort = (int) dataArray[1];
                // 6、A先试探的用原端口，请求下B3次，140ms一次（为了让NAR给B留个记录）
                toClose();
                System.out.println("完成：A断开与Center的连接！");
                // 一定连接失败，3次
                for (int i = 0; i < 3; i++) {
                    try {
                        System.out.println("开始第一次尝试连接B（NAT留下端口映射）");
                        System.out.println("信息：" + targetIp + ":" + targetPort + "本地端口：" + this.localPort);
                        toConnect(targetIp, targetPort, 140, this.localPort);
                        // 不可能连接成功，应为B都没创建ServerSocket
                        System.out.println("此处有个幽灵！！！");
                    } catch (IOException e) {
                        System.out.println("第i次已完成：" + i);
                    }
                    Thread.sleep(130);
                }
                // 7、A用原端口(本地端口)创建ServerSocket
                System.out.println("原端口：" + this.localPort);
                toCreateServiceSocket(this.localPort);
                // 等待B连接
                this.socket = serverSocket.accept();
                System.out.println("B成功接入！");
                // B接入
                this.objectInputStream = new ObjectInputStream(new BufferedInputStream(this.socket.getInputStream()));
                this.objectOutputStream = new ObjectOutputStream(this.socket.getOutputStream());
                this.isOk = true;
                //
                break;
            case 2002:
                // 8、500ms后，B用指定原端口尝试连接5次，400ms一次，如果还是失败，就用再该端口通过中央告诉原主机。
                System.out.println("开始关闭与Center连接！");
                toClose(); // 先关闭原来Socket
                System.out.println("成功关闭，等待0.5S后开始尝试连接");
                Thread.sleep(300);
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("第" + i + "次尝试连接！");
                        toConnect(targetIp, targetPort, 1000, localPort);
                        // 连接成功
                        this.isOk = true;
                        System.out.println("连接成功！");
                        // 发送一条消息
                        TcpMessage newMsg = new TcpMessage();
                        newMsg.code = 2003;
                        newMsg.msg = "我是B，终于连接上你了！";
                        objectOutputStream.writeObject(newMsg);
                        objectOutputStream.flush();
                        break;
                    } catch (IOException e) {
                        System.out.println(i + ":连接失败！");
                    }
                }
                if (this.isOk) {
                    System.out.println("连接失败！！！！！！！！！！！！！！！！！！！！！！！！！！");
                    isActive = false;
                }
                // 9、一般这个时候就已经连接成功，先测试，如果成功就不考虑后面
                break;
        }

    }
}